문단 클릭 = 시작 위치 선택 · 재생은 우하단 버튼/P (재생 중 클릭 = 점프)

EP0. 3주차 — 멀티모달 임베딩: 복합 문서 RAG와 Docker 로컬 Codex

이 학습자료는 강의 자막(강사의 실제 발화)을 중심축으로, 실습 화면의 코드(OCR)와 슬라이드 설명을 보조로 삼아 재구성했다. 코드 식별자·파라미터는 강의 실습 화면에 나온 그대로를 따른다.


0. 사전 필수 용어 (선행지식)

본 주제를 이해하려면 아래 용어를 먼저 잡고 가야 한다. 입문자 기준으로 쉬운 정의 + 비유로 정리한다.

1. 비정형 문서 (Unstructured Document) PDF·스캔본·이미지처럼 행/열이 정해진 표 형식이 아니라, 사람이 읽기 좋게 자유로운 배치로 만들어진 문서. 컴퓨터가 바로 “표는 여기, 본문은 여기, 그림은 여기”라고 구분하지 못한다. → 4장 partition_pdf가 이걸 구조화한다.

2. 임베딩 (Embedding) 글·이미지 같은 데이터를 “의미가 가까우면 숫자도 가까운” 벡터(숫자 배열)로 바꾼 것. 도서관에서 책마다 “주제 좌표”를 매겨 비슷한 주제끼리 옆에 꽂아두는 것과 같다. → 3·4장 벡터 검색의 기반.

3. RAG (Retrieval-Augmented Generation, 검색 증강 생성) LLM이 답하기 전에 관련 문서를 먼저 검색해 와서, 그 근거 위에서 답을 만들게 하는 방식. 시험 볼 때 암기가 아니라 “오픈북으로 찾아보고 답”하는 것. → 2장 문제 정의의 핵심.

4. OCR (Optical Character Recognition, 광학 문자 인식) 사진·스캔 이미지 속 글자를 실제 문자 데이터로 바꾸는 기술. 사진 찍은 책 페이지를 복사-붙여넣기 가능한 텍스트로 만드는 것. → 4장 Tesseract가 담당.

5. 멀티벡터 검색기 (MultiVectorRetriever) “검색용 요약”은 벡터로 저장해 찾기 쉽게 하고, “진짜 원본”은 따로 보관했다가 검색되면 원본을 돌려주는 검색기. 카드 목록(요약)으로 책을 찾고, 실제로는 책(원본)을 빌려주는 도서관 대출 시스템. → 4·6장의 중심 부품.

선행지식이 얕다면 AI 엔지니어링 EP0 - 선수지식을 먼저 보고 오는 것을 권한다. 임베딩·벡터 DB 기초는 이전 회차(ChromaDB·PGVector)에서 다뤘다.


1. 주제 정의

이번 회차의 주제는 멀티모달(멀티모델) 임베딩이다. 텍스트만 다루던 RAG를 넘어, 표와 이미지가 섞인 복합 문서 한 편을 통째로 검색 가능하게 만드는 것이 목표다.

강의는 “대단해 보이지만 실무에서 그렇게까지 어렵게 구현하지 않는다”는 점을 강조한다. 핵심은 두 축이다.

  1. 복합 문서 파싱·요약·검색 파이프라인unstructured로 PDF에서 텍스트·표·이미지를 뽑고, 각각을 LLM으로 요약해 임베딩하고, MultiVectorRetriever로 검색해 멀티모달 답변을 생성한다.
  2. 로컬 환경 운영 — LM Studio의 로컬 LLM만으로 위 파이프라인을 돌리고, AI 코딩 에이전트 Codex를 Docker로 감싸 귀찮은 환경 설정 없이 로컬 모델과 연동한다.

“OpenAI GPT가 없어서 안 된다는 건 다 뻥이고, 작은 로컬 EP 모델로도 이미지·표 포함 멀티모달 RAG가 잘 된다”는 것이 이 강의가 증명하려는 명제다.


2. 풀려는 문제 — 검색 시나리오

일반 텍스트 RAG는 “PDF 안의 표와 그림”을 못 읽는다. 실제 업무 문서(보고서·논문·매뉴얼)는 정작 핵심 정보가 표와 그래프에 있다. 이번 실습의 예제는 질병관리청(KDCA)의 “말라리아 주간소식지 42주차(10.13.~10.19.)” 보고서 PDF이고, 답하고 싶은 질문은 다음과 같다.

질문: “말라리아 군집 사례는 어떤가요?”

이 답은 본문 문장이 아니라 표(주차별 국내 발생·해외 유입 통계)와 현황 이미지에 들어 있다. 그래서 필요한 능력은 다음과 같다.

  • 환각 회피: 문서에 있는 사실(46건, 114명, 4개 시도)만 근거로 답한다.
  • 정확도: 표의 세부 수치를 잃지 않는다.
  • 최신성·private 데이터: 모델이 학습하지 않은 최근·내부 문서도 즉시 검색한다.
  • 멀티모달: 표·이미지까지 검색 대상에 포함한다.

⚠️ 주의 (검색 정확도 함정): 원본 표를 그대로 임베딩하면 숫자 나열이라 “의미 검색”이 잘 안 걸린다. 그래서 이 강의는 원본이 아니라 ‘검색에 최적화된 요약’을 임베딩한다. 검색은 요약으로 하고, LLM에 넘길 근거는 원본으로 되돌린다.


3. 핵심 구조 (멀티모달 RAG 파이프라인)

전체 흐름은 “추출 → 요약 → 이중 저장 → 검색 → 답변”이다.

flowchart LR
    A[PDF 원본] --> B[partition_pdf<br/>레이아웃·OCR·표 파서]
    B --> C1[텍스트 요소]
    B --> C2[표 요소]
    B --> C3[이미지 파일]
    C1 --> D[LLM 텍스트·표 요약]
    C2 --> D
    C3 --> E[LLM 이미지 요약<br/>base64 전달]
    D --> F[요약 → Chroma<br/>임베딩·의미 검색용]
    E --> F
    C1 -. 원본 .-> G[InMemoryStore<br/>docstore]
    C2 -. 원본 .-> G
    C3 -. 원본 .-> G
    F --> H[질문 임베딩·유사도 검색]
    G --> I[MultiVectorRetriever<br/>요약으로 찾고 원본 반환]
    H --> I
    I --> J[프롬프트 구성<br/>텍스트+표+이미지 base64]
    J --> K[LLM 멀티모달 답변]

핵심은 이중 저장이다. vectorstore(Chroma)에는 요약 + 문서 id를, docstore(InMemoryStore)에는 원본 + 같은 id를 넣는다. MultiVectorRetriever가 “요약으로 검색 → id로 원본 회수”를 자동으로 해준다.


4. 구현 가이드 (Do It Yourself)

시작 전

  • Python + LangChain 계열(langchain, langchain-community, langchain-openai, langchain-core), chromadb, nltk 설치.
  • 임베딩·LLM은 LM Studio 로컬 서버를 OpenAI 호환 API로 사용.
  • unstructured는 NLTK·Tesseract를 내부에서 호출한다. 이 둘을 수동으로 깔지 않으면 동작하지 않는다.

패키지 설치:

pip install unstructured[all-docs] langchain langchain-community langchain-openai langchain-core chromadb nltk

💡 실무 노하우 (설치 순서): unstructured[all-docs]로 통째 설치하라. PDF만 부분 설치하면 오히려 동작이 깨진다. NLTK는 pip 설치 후 데이터를 별도로 내려받아야 하고(punkt_tab, averaged_perceptron_tagger_eng), Tesseract는 pip로 안 깔린다 → conda로 설치한다.

import nltk
nltk.download("punkt_tab")
nltk.download("averaged_perceptron_tagger_eng")
# conda 환경이면 conda-forge가 가장 안전 (root/apt 불필요)
conda install -y -c conda-forge tesseract poppler
which tesseract && tesseract --version
which pdftoppm   # poppler 확인

환경 설정 (섹션 0)

import os, uuid, base64, re

LMSTUDIO_BASE_URL = "http://host.docker.internal:12345/v1"  # /v1 포함, LM Studio는 키 검증 안 함
LMSTUDIO_API_KEY  = "lm-studio"
EMBED_MODEL = "embedding-8b"       # LM Studio 로컬 임베딩 별칭
LLM_MODEL   = "gemma4-e2b"         # 강의에서 올린 로컬 LLM 별칭(Gemma 계열)
CHROMA_HOST = "chromadb"
CHROMA_PORT = 8000
FPATH, FNAME = ".", "practice3.pdf"

def strip_think(text: str) -> str:
    if text is None:
        return ""
    return re.sub(r"(^|<think>).*?</think>", "", text, flags=re.DOTALL).strip()

💡 실무 노하우 (strip_think): 싱킹(reasoning) 모델은 출력에 <think>…</think> 블록을 붙인다. 실무 파이프라인에서는 이걸 반드시 잘라내야 한다. 게다가 어떤 모델은 여는 <think> 태그가 누락된 채 처음부터 사고가 나오기도 해서, 정규식을 (^|<think>)로 두어 문서 시작부터 첫 </think>까지도 제거한다.

Step 1 — PDF 요소 추출 (partition_pdf + 캐시)

import os, pickle
from unstructured.partition.pdf import partition_pdf

IMG_DIR = "practice3"
os.makedirs(IMG_DIR, exist_ok=True)
CACHE = os.path.join(IMG_DIR, "raw_pdf_elements.pkl")

if os.path.exists(CACHE):                      # 캐시 있으면 즉시 복원
    with open(CACHE, "rb") as f:
        raw_pdf_elements = pickle.load(f)
else:                                          # 없으면(최초 1회) 풀 실행
    raw_pdf_elements = partition_pdf(
        filename=os.path.join(FPATH, FNAME),
        extract_images_in_pdf=True,            # 이미지도 추출
        infer_table_structure=True,            # 표 구조 복원
        chunking_strategy="by_title",          # 제목 단위 청킹
        extract_image_block_output_dir=IMG_DIR,
        languages=["kor", "eng"],              # 한글 OCR
    )
    with open(CACHE, "wb") as f:
        pickle.dump(raw_pdf_elements, f)
print("총 요소 수:", len(raw_pdf_elements))

partition_pdf의 기본 전략은 strategy="hi_res"다. 내부에서 다음이 순서대로 돈다.

단계 하는 일 무게
레이아웃 탐지 모델(YOLOX) 페이지별 영역 추론 무거움(GPU/CPU)
Tesseract OCR 이미지 있는 페이지에서 글자 인식 무거움(페이지당 수 초)
Table Transformer 표 구조 복원 무거움
pdfminer 텍스트 레이어 추출·머지 가벼움

⚠️ 주의: 이 파싱은 “수십 초 ~ 수 분”이 걸릴 만큼 무겁다. 그래서 결과를 pickle로 캐시해 둔다. 두 번째부터는 pickle.load로 수 밀리초 만에 객체를 복원한다. 무거운 단계 전체를 건너뛰는 것이다.

Step 2 — 표/텍스트 분리

tables, texts = [], []
for element in raw_pdf_elements:
    if "unstructured.documents.elements.Table" in str(type(element)):
        tables.append(str(element))
    elif "unstructured.documents.elements.CompositeElement" in str(type(element)):
        texts.append(str(element))
print("tables:", len(tables), "texts:", len(texts))   # 예: tables: 10  texts: 35

Step 3 — 텍스트·표 요약 (검색 최적화)

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

prompt_text = """당신은 표와 텍스트를 요약하여 검색할 수 있도록 돕는 어시스턴트입니다.
이 요약은 임베딩되어 원본 텍스트나 표 요소를 검색하는 데 사용될 것입니다.
표 또는 텍스트에 대한 간결한 요약을 제공하여 검색에 최적화된 형태로 만들어 주세요.
표 또는 텍스트: {element}"""
prompt = ChatPromptTemplate.from_template(prompt_text)

# 입력 element → 프롬프트 → LLM → 문자열 → <think> 제거
summarize_chain = {"element": lambda x: x} | prompt | llm | StrOutputParser() | (lambda s: strip_think(s))

text_summaries  = summarize_chain.batch(texts,  {"max_concurrency": 5}) if texts  else []
table_summaries = summarize_chain.batch(tables, {"max_concurrency": 5}) if tables else []

💡 실무 노하우 (동시성): max_concurrency는 LM Studio 서버가 감당하는 만큼 올리면 배열을 병렬로 훑어 빨라진다. 강의 실행에서는 5 → 20까지 올렸다. 요소가 많으니(표 10 + 텍스트 35) 병렬 이득이 크다.

Step 4 — 이미지 요약 (base64) → LLM

def encode_image(image_path: str) -> str:
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode("utf-8")

img_base64_list = []
for img_file in sorted(os.listdir(IMG_DIR)):
    if img_file.lower().endswith((".jpg", ".jpeg", ".png")):
        img_base64_list.append(encode_image(os.path.join(IMG_DIR, img_file)))
print("이미지 수:", len(img_base64_list))     # 예: 43

from langchain_core.messages import HumanMessage

def image_summarize(img_base64: str) -> str:
    msg = llm.invoke([HumanMessage(content=[
        {"type": "text", "text": IMAGE_PROMPT},
        {"type": "image_url",
         "image_url": {"url": f"data:image/jpeg;base64,{img_base64}"}},
    ])])
    return strip_think(msg.content)

image_summaries = [image_summarize(b) for b in img_base64_list]

⚠️ 주의 (URL 금지, base64 필수): 책 예제는 이미지 URL을 넘기지만 LM Studio 같은 로컬 LLM API는 외부 URL을 안 받아준다. URL을 이미지로 입력받으려면 서버가 바깥 URL을 가져오는 기능을 붙여야 하는데, 로컬 서버들은 대개 구현하지 않고 Base64 data URL만 받는다. 그래서 선택의 여지 없이 이미지를 base64로 인코딩해 넘긴다.

Step 5 — 멀티벡터 검색기 구성 + 원본 저장

import chromadb
from langchain_core.stores import InMemoryStore
from langchain_core.documents import Document
from langchain_community.vectorstores import Chroma   # deprecation 경고만, 동작함

# MultiVectorRetriever: LangChain 1.x에서 위치가 옮겨져 폴백 처리
try:
    from langchain_classic.retrievers.multi_vector import MultiVectorRetriever
except ImportError:
    try:
        from langchain.retrievers.multi_vector import MultiVectorRetriever
    except ImportError:
        from langchain.retrievers import MultiVectorRetriever

chroma_client = chromadb.HttpClient(host=CHROMA_HOST, port=CHROMA_PORT)
vectorstore = Chroma(client=chroma_client,
                     collection_name="multi_modal_rag",
                     embedding_function=embeddings)
docstore = InMemoryStore()
id_key = "doc_id"
retriever = MultiVectorRetriever(vectorstore=vectorstore, docstore=docstore, id_key=id_key)

원본 저장은 각 요소마다 고유 id를 만들고, docstore.mset으로 (id, 원본) 쌍을, vectorstore에는 (요약 + 같은 id) 문서를 넣는다.

💡 실무 노하우 (쿨다운 배치): 임베딩을 한 번에 20개씩 쏟으면 VRAM 4GB급 로컬 서버는 죽는다. 강의에서는 safe_add에서 배치 4로 제약을 걸어 “한 번에 4개씩만 진행”하도록 이중 루프로 쿨다운을 준다. 로컬 운영의 필수 방어다.

Step 6 — 검색·멀티모달 답변

from base64 import b64decode

def split_image_text_types(docs):
    """base64 디코딩 성공 여부로 이미지/텍스트 분리"""
    b64, text = [], []
    for doc in docs:
        try:
            b64decode(doc, validate=True)
            b64.append(doc)
        except Exception:
            text.append(doc)
    return {"images": b64, "texts": text}
from langchain_core.runnables import RunnablePassthrough, RunnableLambda

def prompt_func(data):
    format_texts = "\n".join(data["context"]["texts"])
    text = (f"다음 문맥에만 기반하여 질문에 답하세요. 문맥에는 텍스트, 표, 그리고 "
            f"아래 이미지가 포함될 수 있습니다.\n질문: {data['question']}\n"
            f"텍스트와 표: {format_texts}")
    content = [{"type": "text", "text": text}]
    if data["context"]["images"]:
        content.append({"type": "image_url",
            "image_url": {"url": f"data:image/jpeg;base64,{data['context']['images'][0]}"}})
    return [HumanMessage(content=content)]

chain = (
    {"context": retriever | RunnableLambda(split_image_text_types),
     "question": RunnablePassthrough()}
    | RunnableLambda(prompt_func) | llm | StrOutputParser() | (lambda s: strip_think(s))
)
answer = chain.invoke("말라리아 군집 사례는 어떤가요?")
print(answer)

확인: 실습 결과, 질문에 대해 관련 이미지(“말라리아 군집사례 현황”)가 검색되어 튀어나오고, 답변은 “42주차(10.13.~10.19.) 동안 4개 시도(경기·서울·인천·강원)에서 총 46건, 114명”을 정확히 집어냈다. 가장 작은 로컬 모델로도 표·이미지 근거 답변이 나온다는 증거다.


5. 적용 사례 (RAG·벡터 DB OSS)

이 파이프라인에서 실제로 쓰거나 대체 가능한 공신력 있는 오픈소스들이다.

  • LangChain (https://github.com/langchain-ai/langchain) — 본 실습의 오케스트레이션(체인·리트리버).
  • LlamaIndex (https://github.com/run-llama/llama_index) — 인덱싱·쿼리 엔진 대안.
  • Chroma (https://github.com/chroma-core/chroma) — 본 실습의 벡터 스토어.
  • Weaviate (https://github.com/weaviate/weaviate) — 하이브리드 검색이 강한 대안.
  • Qdrant (https://github.com/qdrant/qdrant) — 고성능 러스트 벡터 DB.
  • FAISS (https://github.com/facebookresearch/faiss) — 로컬 인메모리 유사도 검색.
  • sentence-transformers (https://github.com/UKPLab/sentence-transformers) — 로컬 임베딩 모델.

📚 참고: 상용 AI 회사(예: 업스테이지 Solar)가 “PDF 던지면 마크다운으로, 표는 문장으로, 그래프는 설명으로” 바꿔주는 기능도 결국 이 부류의 문서 파싱을 한다. unstructured를 쓰면 별도 LLM 없이 거의 같은 결과를 얻을 수 있다.


6. 핵심 원리 — 요약을 검색하고 원본으로 답한다

멀티벡터 검색의 원리를 한 문장으로 요약하면: “검색 성능은 요약이, 답변 근거는 원본이 담당한다.”

  • 원본 표를 그대로 임베딩하면 숫자·기호 나열이라 질문의 의미와 벡터 거리가 멀어 잘 안 걸린다.
  • 그래서 각 표·텍스트·이미지를 자연어 요약으로 바꿔 임베딩한다(의미 검색 최적화).
  • 검색에 걸린 요약의 doc_iddocstore에서 원본을 회수해 LLM에 근거로 넘긴다.

이 구조가 “하이브리드” 사상의 한 형태다. 전통 RAG의 확장으로, 키워드(BM25) + 벡터(dense) 를 함께 쓰는 Hybrid Retrieval도 같은 “서로 다른 강점을 결합” 정신을 공유한다.

💡 실무 노하우 (동시성 vs KV 캐시): 모델을 로딩할 때 parallel 값을 올리면 세션을 그 배수로 유지하므로 KV 캐시도 배수로 증가한다. 한 세션에 KV 2GB가 필요했다면 20 병렬이면 그만큼 커진다. 작은 모델이라 20을 줬을 뿐, 큰 모델은 오히려 병렬을 1로 묶어 한 번에 하나만 돌린다.


7. 변형 — 파싱 전략과 검색 고도화

  • hi_res vs fast: fast로 바꾸면 Tesseract·레이아웃 모델을 안 쓰고 내장 모듈로 빠르게 처리한다. 정확도↓ 속도↑. 스캔 이미지가 없는 텍스트 PDF면 fast로 충분하다.
  • PDFminer + Tesseract 역할 분담: 텍스트 레이어가 있으면 pdfminer가 가볍게 추출하고, 스캔된 PDF(텍스트 레이어 없음) 는 Tesseract가 OCR로 글자를 만든다. 둘은 대체재가 아니라 분업이다.
  • 요약 프롬프트 튜닝 / HyDE / RAG-Fusion / Multi-vector: 검색 리콜을 올리는 상위 기법. 본 실습의 “요약 임베딩”이 Multi-vector 계열의 실전 사례다.
  • 인퍼런스 싱크 끄기: 요약처럼 짧은 출력만 필요한 작업은 모델의 thinking을 끄면 출력 토큰이 확 줄어 훨씬 빨라진다.

8. 다른 접근과의 비교

접근 언제 유리 한계
멀티모달 RAG(본 실습) 표·이미지 포함 최신·내부 문서를 근거로 답할 때 파이프라인·검색 품질 관리 필요
Fine-tuning 말투·형식·도메인 스타일을 모델에 각인할 때 사실 최신성·출처 추적 약함, 재학습 비용
Long context(문서 통째 주입) 문서가 작고 매번 전부 필요할 때 토큰·비용 폭증, 컨텍스트 낭비, 긴 문서 불가
Function calling 실시간 API·계산이 필요할 때 “문서 검색” 자체는 별도 필요

핵심: 최신성·출처·표/이미지 근거가 필요하면 RAG, 스타일 내재화가 필요하면 파인튜닝. 둘은 배타적이지 않고 병행한다.


9. 한계

  • 검색 품질 의존성: 요약이 부실하면 원본을 못 찾는다. 요약 프롬프트가 사실상 검색 성능을 좌우한다.
  • chunk boundary 오류: by_title 청킹이 문맥을 어중간하게 끊으면 답이 조각난다.
  • OCR 인식 오류: Tesseract는 전처리(스레숄드·스케일업)를 안 하면 정확도가 떨어진다. 요즘은 LLM-OCR이 더 낫다는 흐름도 있다.
  • 로컬 자원 한계: VRAM이 작으면 동시성·모델 크기가 제약된다. 임베딩을 한꺼번에 던지면 서버가 죽으므로 배치·쿨다운이 필수.
  • 다국어·도메인 어휘: NLTK는 본래 영어권 최적화라 한글 처리가 약하다(형태소 분석은 Mecab 계열이 강함).

⚠️ 주의: “컴퓨터가 후져서 안 된다”는 대부분 자원 부족 신호다. 최소한의 VRAM·메모리 없이 멀티모달 파싱+로컬 LLM을 동시에 돌리는 건 어렵다.


10. 최신 패턴 (2026-08 기준) & Docker 로컬 Codex

검색 고도화 방향

  • Agentic RAG — 검색·재검색·도구 호출을 에이전트가 스스로 판단.
  • GraphRAG — 문서를 지식 그래프로 만들어 관계 기반 검색.
  • Self-RAG / CRAG — 검색 결과의 신뢰도를 모델이 자체 평가·교정.

Docker로 로컬 Codex 운영 (강의 후반부)

오픈소스 Codex(러스트 구현, 진짜 Codex와 도구가 거의 동일)를 Docker로 감싸 로컬 LM Studio 모델과 연동한다. 목적은 “귀찮은 environment 설정·보안 걱정 없이, 호스트를 더럽히지 않고” 쓰기 위해서다.

FROM node:22
RUN npm install -g @openai/codex   # 컨테이너 안에 글로벌 설치
WORKDIR /workspace

docker-compose에는 볼륨 두 개(작업용 workspace, codex 홈)와 stdin_open/tty를 잡는다. 그리고 config.toml(Codex 홈)에서 로컬 서버를 가리킨다.

# config.toml (핵심만)
model = "gemma4-e2b"
model_provider = "lmstudio-local"
approval_policy = "never"          # 도커 안이라 위험 없음 → danger-full-access
model_context_window = 262000      # 안 주면 100K로 오인식
model_max_output_tokens = 34000    # 컴팩션 시점 지정

[model_providers.lmstudio-local]
base_url = "http://host.docker.internal:12345/v1"
wire_api = "responses"             # responses-api만 지원, chat completion 미지원

⚠️ 주의 (컨텍스트 오인식): model_context_window를 지정하지 않으면 Codex는 로컬 모델을 무조건 100K로 오인식한다. LM Studio에서 262K로 크게 로딩한 보람이 사라진다. 컴팩션(요약 압축) 시점을 위해 최대 출력 토큰도 직접 준다.

Codex가 실제로 보내는 요청을 열어보면 시스템 프롬프트가 거대하다: 기본 인스트럭션(“너는 코딩 에이전트, Codex CLI에서 실행 중”, update_plan 지침) → 퍼미션 인스트럭션 → 협업 모드 → 스킬 인스트럭션 → environment context(작업 경로·실행 방법·퍼미션) → 그제서야 사용자 메시지. 도구는 커맨드 실행·파일 쓰기·사용자 질문·이미지 뷰어·goal 지정까지 세트로 들어온다.

💡 실무 노하우 (프롬프트 프로세싱 체감): 로컬 모델이 “느리게” 느껴지는 건 대부분 프롬프트 프로세싱(입력을 다 읽는 단계)이 길기 때문이지, 토큰 생성 속도 문제가 아니다. 프리픽스 캐시를 깨면 프롬프트 프로세싱을 다시 하므로 훨씬 느려진다. 컨텍스트는 아무것도 안 해도 로딩만으로 25K가 날아가고, 도구 몇 번 쓰면 50~60K가 훅 사라진다.


11. 메타인지 자기평가

배운 것을 실제로 할 수 있는지 스스로 점검하라.

  1. 검색 정확도 측정: recall@K, MRR로 “요약 임베딩 → 원본 회수”가 실제 질문에 맞는 근거를 얼마나 데려오는지 측정한다.
  2. Re-ranker 추가 후 재측정: 검색된 후보를 재순위(Cohere·BGE 등)로 다시 정렬하고 지표가 오르는지 비교한다.
  3. 체크리스트
  4. [ ] unstructured가 NLTK·Tesseract를 왜 필요로 하는지 설명할 수 있는가?
  5. [ ] hi_res/fast 전략을 상황에 맞게 선택할 수 있는가?
  6. [ ] 이미지를 URL이 아니라 base64로 넘겨야 하는 이유를 아는가?
  7. [ ] MultiVectorRetriever에서 요약과 원본이 각각 어디에 저장되는지 그릴 수 있는가?
  8. [ ] 로컬 서버가 죽지 않도록 배치·쿨다운·동시성을 조절할 수 있는가?
  9. [ ] Codex를 Docker로 감싸고 config.toml로 로컬 모델을 연결할 수 있는가?

확인: 위 6개를 모두 “예”라고 답할 수 있으면 이번 회차의 목표(작은 로컬 모델로도 멀티모달 RAG 구축)에 도달한 것이다.

난이도
에피소드
질문
카드를 로딩 중...
답변

클릭하거나 Space를 눌러 뒤집기

0 / 0
학습 진도 0%
이동   Space 뒤집기   R 셔플   P 음성 버전 2026-08-05b